import {beforeEach, jest, test} from '@jest/globals';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import {confirmReadyToOpenApp, openApp, reconnectApp} from '@libs/actions/App';
import {buildOldDotURL, openExternalLink} from '@libs/actions/Link';
import OnyxUpdateManager from '@libs/actions/OnyxUpdateManager';
import {getAll as getAllPersistedRequests} from '@libs/actions/PersistedRequests';
// eslint-disable-next-line no-restricted-syntax
import * as SignInRedirect from '@libs/actions/SignInRedirect';
import {WRITE_COMMANDS} from '@libs/API/types';
import asyncOpenURL from '@libs/asyncOpenURL';
import HttpUtils from '@libs/HttpUtils';
import PushNotification from '@libs/Notification/PushNotification';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
import '@libs/Notification/PushNotification/subscribeToPushNotifications';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import * as SessionUtil from '@src/libs/actions/Session';
import {KEYS_TO_PRESERVE_SUPPORTAL, signOutAndRedirectToSignIn} from '@src/libs/actions/Session';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Credentials, Session} from '@src/types/onyx';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';

// We are mocking this method so that we can later test to see if it was called and what arguments it was called with.
// We test HttpUtils.xhr() since this means that our API command turned into a network request and isn't only queued.
HttpUtils.xhr = jest.fn<typeof HttpUtils.xhr>();

// Mocked to ensure push notifications are subscribed/unsubscribed as the session changes
jest.mock('@libs/Notification/PushNotification');

// Mocked to check SignOutAndRedirectToSignIn behavior
jest.mock('@libs/asyncOpenURL');

jest.mock('@libs/actions/Link', () => {
    return {
        buildOldDotURL: jest.fn(() => Promise.resolve('mockOldDotURL')),
        openExternalLink: jest.fn(),
    };
});

Onyx.init({
    keys: ONYXKEYS,
});

OnyxUpdateManager();
beforeEach(() => Onyx.clear().then(waitForBatchedUpdates));

describe('Session', () => {
    test('Authenticate is called with saved credentials when a session expires', async () => {
        // Given a test user and set of authToken with subscriptions to session and credentials
        const TEST_USER_LOGIN = 'test@testguy.com';
        const TEST_USER_ACCOUNT_ID = 1;
        const TEST_INITIAL_AUTH_TOKEN = 'initialAuthToken';
        const TEST_REFRESHED_AUTH_TOKEN = 'refreshedAuthToken';

        let credentials: OnyxEntry<Credentials>;
        Onyx.connect({
            key: ONYXKEYS.CREDENTIALS,
            callback: (val) => (credentials = val),
        });

        let session: OnyxEntry<Session>;
        Onyx.connect({
            key: ONYXKEYS.SESSION,
            callback: (val) => (session = val),
        });

        // When we sign in with the test user
        await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN);
        await waitForBatchedUpdates();

        // Then our re-authentication credentials should be generated and our session data
        // have the correct information + initial authToken.
        expect(credentials?.login).toBe(TEST_USER_LOGIN);
        expect(credentials?.autoGeneratedLogin).not.toBeUndefined();
        expect(credentials?.autoGeneratedPassword).not.toBeUndefined();
        expect(session?.authToken).toBe(TEST_INITIAL_AUTH_TOKEN);
        expect(session?.accountID).toBe(TEST_USER_ACCOUNT_ID);
        expect(session?.email).toBe(TEST_USER_LOGIN);

        // At this point we have an authToken. To simulate it expiring we'll just make another
        // request and mock the response so it returns 407. Once this happens we should attempt
        // to Re-Authenticate with the stored credentials. Our next call will be to Authenticate
        // so we will mock that response with a new authToken and then verify that Onyx has our
        // data.
        (HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)

            // This will make the call to OpenApp below return with an expired session code
            .mockImplementationOnce(() =>
                Promise.resolve({
                    jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED,
                }),
            )

            // The next call should be Authenticate since we are re-authenticating
            .mockImplementationOnce(() =>
                Promise.resolve({
                    jsonCode: CONST.JSON_CODE.SUCCESS,
                    accountID: TEST_USER_ACCOUNT_ID,
                    authToken: TEST_REFRESHED_AUTH_TOKEN,
                    email: TEST_USER_LOGIN,
                }),
            );

        // When we attempt to fetch the initial app data via the API
        confirmReadyToOpenApp();
        openApp();
        await waitForBatchedUpdates();

        // Then it should fail and reauthenticate the user adding the new authToken to the session
        // data in Onyx
        expect(session?.authToken).toBe(TEST_REFRESHED_AUTH_TOKEN);
    });

    test('Push notifications are subscribed after signing in', async () => {
        await TestHelper.signInWithTestUser();
        await waitForBatchedUpdates();
        expect(PushNotification.register).toHaveBeenCalled();
    });

    test('Push notifications are unsubscribed after signing out', async () => {
        await TestHelper.signInWithTestUser();
        await TestHelper.signOutTestUser();
        expect(PushNotification.deregister).toHaveBeenCalled();
    });

    test('ReconnectApp should push request to the queue', async () => {
        await TestHelper.signInWithTestUser();
        await Onyx.set(ONYXKEYS.HAS_LOADED_APP, true);
        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

        confirmReadyToOpenApp();
        reconnectApp();

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(1);
        expect(getAllPersistedRequests().at(0)?.command).toBe(WRITE_COMMANDS.RECONNECT_APP);

        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(0);
    });

    test('ReconnectApp should open if app is not loaded', async () => {
        await TestHelper.signInWithTestUser();
        await Onyx.set(ONYXKEYS.HAS_LOADED_APP, false);
        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

        confirmReadyToOpenApp();
        reconnectApp();

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(1);
        expect(getAllPersistedRequests().at(0)?.command).toBe(WRITE_COMMANDS.OPEN_APP);

        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(0);
    });

    test('ReconnectApp should replace same requests from the queue', async () => {
        await TestHelper.signInWithTestUser();
        await Onyx.set(ONYXKEYS.HAS_LOADED_APP, true);
        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

        confirmReadyToOpenApp();
        reconnectApp();
        reconnectApp();
        reconnectApp();
        reconnectApp();

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(1);
        expect(getAllPersistedRequests().at(0)?.command).toBe(WRITE_COMMANDS.RECONNECT_APP);

        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});

        expect(getAllPersistedRequests().length).toBe(0);
    });

    test('OpenApp should push request to the queue', async () => {
        await TestHelper.signInWithTestUser();
        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

        openApp();

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(1);
        expect(getAllPersistedRequests().at(0)?.command).toBe(WRITE_COMMANDS.OPEN_APP);

        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(0);
    });

    test('OpenApp should replace same requests from the queue', async () => {
        await TestHelper.signInWithTestUser();
        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

        openApp();
        openApp();
        openApp();
        openApp();

        await waitForBatchedUpdates();

        expect(getAllPersistedRequests().length).toBe(1);
        expect(getAllPersistedRequests().at(0)?.command).toBe(WRITE_COMMANDS.OPEN_APP);

        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});

        expect(getAllPersistedRequests().length).toBe(0);
    });

    test('SignOut should return a promise with response containing hasOldDotAuthCookies', async () => {
        await TestHelper.signInWithTestUser();
        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

        (HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)
            // This will make the call to OpenApp below return with an expired session code
            .mockImplementationOnce(() =>
                Promise.resolve({
                    jsonCode: CONST.JSON_CODE.SUCCESS,
                    hasOldDotAuthCookies: true,
                }),
            );

        const signOutPromise = SessionUtil.signOut();

        expect(signOutPromise).toBeInstanceOf(Promise);

        expect(await signOutPromise).toStrictEqual({
            jsonCode: CONST.JSON_CODE.SUCCESS,
            hasOldDotAuthCookies: true,
        });

        await Onyx.set(ONYXKEYS.NETWORK, {isOffline: false});

        expect(getAllPersistedRequests().length).toBe(0);
    });

    describe('SignOutAndRedirectToSignIn', () => {
        test('SignOutAndRedirectToSignIn should redirect to OldDot when LogOut returns truthy hasOldDotAuthCookies', async () => {
            await TestHelper.signInWithTestUser();
            await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

            (HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)
                // This will make the call to OpenApp below return with an expired session code
                .mockImplementationOnce(() =>
                    Promise.resolve({
                        jsonCode: CONST.JSON_CODE.SUCCESS,
                        hasOldDotAuthCookies: true,
                    }),
                );

            const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve());

            signOutAndRedirectToSignIn();

            await waitForBatchedUpdates();

            expect(asyncOpenURL).toHaveBeenCalledWith(expect.any(Promise), `${CONFIG.EXPENSIFY.EXPENSIFY_URL}${CONST.OLDDOT_URLS.SIGN_OUT}`, true, true);
            expect(redirectToSignInSpy).toHaveBeenCalled();
            jest.clearAllMocks();
        });

        test('SignOutAndRedirectToSignIn should not redirect to OldDot when LogOut return falsy hasOldDotAuthCookies', async () => {
            await TestHelper.signInWithTestUser();
            await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true});

            (HttpUtils.xhr as jest.MockedFunction<typeof HttpUtils.xhr>)
                // This will make the call to OpenApp below return with an expired session code
                .mockImplementationOnce(() =>
                    Promise.resolve({
                        jsonCode: CONST.JSON_CODE.SUCCESS,
                    }),
                );

            const redirectToSignInSpy = jest.spyOn(SignInRedirect, 'default').mockImplementation(() => Promise.resolve());

            signOutAndRedirectToSignIn();

            await waitForBatchedUpdates();

            expect(asyncOpenURL).not.toHaveBeenCalled();
            expect(redirectToSignInSpy).toHaveBeenCalled();
            jest.clearAllMocks();
        });

        test('SignOutAndRedirectToSignIn should restore stashed session and redirect to OldDot supportal of supportal agent', async () => {
            jest.spyOn(SessionUtil, 'isSupportAuthToken').mockReturnValue(true);
            jest.spyOn(SessionUtil, 'hasStashedSession').mockReturnValue(true);
            jest.spyOn(SessionUtil, 'signOut').mockResolvedValue(undefined);
            jest.spyOn(Onyx, 'clear').mockResolvedValue(undefined);
            jest.spyOn(Onyx, 'multiSet').mockResolvedValue(undefined);

            const testStashedCredentials = {login: 'stashed@expensify.com', autoGeneratedLogin: 'stashedAutoLogin', autoGeneratedPassword: 'stashedAutoPassword'};
            const testStashedSession = {authToken: 'stashedAuthToken', email: 'stashed@expensify.com', accountID: 123, creationDate: new Date().getTime()};

            await Onyx.set(ONYXKEYS.STASHED_CREDENTIALS, testStashedCredentials);
            await Onyx.set(ONYXKEYS.STASHED_SESSION, testStashedSession);
            await Onyx.merge(ONYXKEYS.SESSION, {authTokenType: CONST.AUTH_TOKEN_TYPES.SUPPORT});

            await waitForBatchedUpdates();

            const onyxClearSpy = Onyx.clear as jest.Mock;
            const onyxMultiSetSpy = Onyx.multiSet as jest.Mock;

            signOutAndRedirectToSignIn(false, false, true, true);

            await waitForBatchedUpdates();

            expect(SessionUtil.signOut).not.toHaveBeenCalled();

            expect(onyxClearSpy).toHaveBeenCalledWith(KEYS_TO_PRESERVE_SUPPORTAL);

            expect(onyxMultiSetSpy).toHaveBeenCalledWith({
                [ONYXKEYS.CREDENTIALS]: testStashedCredentials,
                [ONYXKEYS.SESSION]: testStashedSession,
            });

            expect(buildOldDotURL).toHaveBeenCalledWith(CONST.OLDDOT_URLS.SUPPORTAL_RESTORE_STASHED_LOGIN);
            expect(openExternalLink).toHaveBeenCalledWith('mockOldDotURL', undefined, true);
        });
    });
});
